// --------------------------------------------------------------------------------------------------------------------
// <copyright file="UdpSender.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Runtime
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Runtime.Serialization;
    using System.Xml;
    using Microsoft.Robotics.IO;

    /// <summary>
    /// A weakly-typed forwarder that serializes a message and sends it to a remote ip/port as a UDP packet.
    /// This forwarder can be registered as a subscriber to a broadcast forwarder,
    /// to publish messages from a local agent to a remote machine.
    /// The class doesn't perform any chunking. Serialized Agent Messages must fit in 64K.
    /// </summary>
    internal class UdpSender : IForwarder, IDisposable
    {
        /// <summary>
        /// Maximum size of a datagram; taking worst case UDP and IP headers into account.
        /// </summary>
        public const int MaxUdpDatagramSize = ushort.MaxValue - 8 /* UDP header */ - 20 /* IP header */ - 48 /* pseudo header */;

        /// <summary>
        /// Maximum number of bytes for a serialized message.
        /// </summary>
        public const int MaxSerializedBytes = MaxUdpDatagramSize * 100;

        /// <summary>
        /// IP endpoint with which to communicate.
        /// </summary>
        private readonly IPEndPoint endpoint;

        /// <summary>
        /// Chunking sender for handling large payloads.
        /// </summary>
        private DataChunker chunker;

        /// <summary>
        /// The data contract serializer to use.
        /// </summary>
        private DataContractSerializer serializer;

        /// <summary>
        /// The buffer used to serialize the messages.
        /// </summary>
        private MemoryStream stream;

        /// <summary>
        /// Initializes a new instance of the UdpSender class.
        /// </summary>
        /// <param name="endpoint">remote endpoint to send the message to.</param>
        /// <param name="types">The types of messages that will be sent using this sender.</param>
        public UdpSender(IPEndPoint endpoint, IEnumerable<Type> types)
        {
            this.endpoint = endpoint;
            this.serializer = new DataContractSerializer(typeof(AgentMessage), types, Serializer.MaxItemsInTheObjectGraph, true, false, new SerializationSurrogate(false));
            this.chunker = new DataChunker(MaxUdpDatagramSize);
            this.stream = new MemoryStream(MaxSerializedBytes); // ends up on the large object heap (don't need to pin).
        }

        /// <summary>
        /// Disposes of an instance of the <see cref="UdpSender"/> class.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
        }

        /// <summary>
        /// Accepts and sends over the wire a message of the given type. 
        /// This method is meant for streaming data over the wire. 
        /// For performance reasons and to keep things simple, there is no reliability contract and the method doesn't provide any confirmation of delivery. 
        /// If we ever need reliable message passing over the network, we should implement a separate TCP Sender/Receiver and associated proxies.
        /// </summary>
        /// <param name="message">Message to forward/</param>
        /// <param name="blockUntilDelivered">Not used, we always block until the message is sent over the wire</param>
        public void Post(object message, bool blockUntilDelivered = false)
        {
            using (var udpClient = new UdpClient())
            {
                udpClient.Connect(this.endpoint);
                ////Console.WriteLine("Send: {0}", this.udpClient.Client.RemoteEndPoint);

                // TODO: need to switch to binary encoding, but couldn't get XmlBinaryWriter to work yet.
                using (XmlDictionaryWriter writer = XmlDictionaryWriter.CreateBinaryWriter(this.stream, null, null, false))
                {
                    this.serializer.WriteObject(writer, message);
                }

                if (this.stream.Length > MaxSerializedBytes)
                {
                    throw new InvalidDataException("Message exceeded MaxSerializedBytes.");
                }

                // This assumes the message is an AgentMessage and that it is the same *version* as this assembly.
                // TODO: Fix this by making UdpSender itself a regular agent.
                foreach (var chunk in this.chunker.GetChunks(((AgentMessage)message).Id, this.stream.GetBuffer(), (int)this.stream.Position))
                {
                    udpClient.Send(chunk.Item1, chunk.Item2);
                }

                this.stream.Seek(0, SeekOrigin.Begin);
            }
        }

        /// <summary>
        /// Disposes of an instance of the <see cref="UdpSender"/> class.
        /// </summary>
        /// <param name="disposing">True if disposing, false if finalizing.</param>
        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.stream != null)
                {
                    this.stream.Dispose();
                }

                GC.SuppressFinalize(this);
            }
        }
    }
}
